
I wanted to explore a simple demand model, the package HoloViews can be used to enable a quick visualisation.
Unfortunately the HoloViews module doesn't seem to play nicely with my notebook generation software. As a stopgap you can find the rendered notebook here, be warned that you will leave my sites theme behind!
The aim of this notebook is to explore the notion of how housing demand, constrained supply and interest rates effect things. We are using the mpld3 backend described here. This notebook was rendered using the development version 0.3 from github.
import holoviews as hv
import holoviews.operation as op
import numpy as np
import holoviews.plotting.hooks
%load_ext holoviews.ipython
%output backend='d3' size=100 dpi=150
So the parameters are going to be, percentage of annual income, amount of income inequality, current interest rates.
wtp = np.arange(0.1, 0.6, 0.05)
steepness_of_income_drop = np.arange(0.6, 1, 0.05)
current_interest_rates = np.arange(0, 0.75, 0.05)
num_houses = range(2,10)
wtp = [0.4, 0.5, 0.6, 0.7]
steepness_of_income_drop = [0.6, 0.7, 0.8]
current_interest_rates =[.02, 0.03, 0.05]
num_houses = [1, 4, 6, 8]
keys = []
for w in wtp:
for s in steepness_of_income_drop:
for c in current_interest_rates:
for n in num_houses:
keys.append((w,s,c,n))
minimum_house_cost = 1e5
median_income = 65e3
Lets assume that there are 9 people, the median person earns 65000 per year.
def make_incomes(median, steepness_of_income_drop):
"""
This function will generate a distribution of incomes in an inequal society. We set the
median, and every person earns only a proportion of the previous persons income.
:param median: Median income of the group
:param steepness_of_income_drop: A float in the range (0, 1), a number close to 0 is
a very inequal society, a number close to 1 is a very
equal society.
:returns: A list of incomes of length 9.
"""
bottom_4 = []
prev = median
for i in range(4):
bottom_4.append(prev*steepness_of_income_drop)
prev = bottom_4[-1]
top_4 = []
prev = median
for i in range(4):
top_4.append(prev/steepness_of_income_drop)
prev = top_4[-1]
incomes = bottom_4
incomes.extend(top_4)
incomes.append(median)
incomes = np.sort(incomes)
return incomes
def add_x_index(y):
"""
Adds an X index in a manner suitable for holoviews Curve element.
:params y: vector to be appended
:returns: a list of lists of length two, representing coordinates.
"""
y = y.tolist()
return zip(range(len(y)), y)
The housing model is implemented below. Basically it states that the price of all houses is $1 more than the poorest person can affort if demand is constrained, otherwise it is set to some marginal minimum cost.
def calculate_house_cost(incomes, minimum_house_cost, num_houses, interest_rate, wtp):
"""
If there are an equal number of houses and people, then the house costs whatever the
marginal cost is for a house. If there are not enough houses then the price of the house
is $1 more than the poorest person is willing to pay.
:param incoms: A list of incomes for each buyer.
:param minimum_house_cost: The minimum cost to build a house.
:param interest_rate: The current interest rate
:param wtp: The willingness to pay (for everyone) as a proportion of their income (0-1)
:returns: A list of house prices for each person. $0 if a person does not buy a house.
"""
house_cost = np.zeros(len(incomes))
if num_houses >= len(incomes):
house_cost = np.ones(len(incomes)) * minimum_house_cost
return house_cost
else:
# Now we have to bargain.
# calculate each person's willingness to pay (absolute value)
WTP = np.array(incomes) * np.array(wtp)
# Now, calculate how much they could borrow given that.
WTP = WTP/interest_rate # represents the max that they could borrow to buy the house.
# Now assume that they require (and can access at 15% deposit)
WTP = WTP / 0.85
winners = np.argsort(WTP)
WTP = np.sort(WTP)
# The new house price is equal to the value that the person who misses out plus 1.
house_cost = (WTP[len(incomes) - num_houses] + 1)*np.ones(len(incomes))/1e3
house_cost[0:len(incomes)-num_houses] = 0
return zip(range(len(incomes)), house_cost)
res = calculate_house_cost(make_incomes(median_income, 0.7), 1e5, 5, 0.5, 0.70)
# Build holo-views data.
d1 = {k: hv.Scatter(calculate_house_cost(make_incomes(median_income, k[1]), minimum_house_cost, k[3], k[2], k[0]),
key_dimensions=['house location'], value_dimensions=['House Price (\'000)'])
for k in keys }
d2 = {k[1]: hv.Curve(add_x_index(make_incomes(median_income, k[1]))) for k in keys }
h1 = hv.HoloMap(d1, key_dimensions = ['wtp', 'income_drop', 'interest_rate', 'num_houses'])
h2 = hv.HoloMap(d2, key_dimensions=[ 'income_drop'])
%%opts Layout [figure_size=100 figure_bounds=(0.1,0,1,1)]
# Generate holoview plots
h1+h2